路径引导 室外模拟导航

最后更新时间:2019年7月5日

功能描述

路径引导,即导航功能,基于已有的规划路径实现导航,按照规划路径行进,辅以文字、图标引导,以及语音播报功能等。MapGIS Mobile SDK提供室内外一体化的路径引导接口,并且提供模拟导航,接入定位数据后可实现真实导航。此外,还能提供丰富的指引信息,比如当前道路的名称、当前导航动作、到达终点的距离等;提供路口放大图来准确指引路线;同时支持语音播报操作,提供更加友好、科学的智能化引导服务。

实现方法

路径引导(导航)功能实现的一般流程如下图所示:

路径引导实现流程.png

1

路径引导对象准备

构造路径引导对象MGSRouteGuide,并初始化路径引导对象,设置路线对象MGSRoute、路径分析对象MGSRouteAnalysis两个参数;

//构造路径引导对象
MGSRouteGuide *routeGuide=[[MGSRouteGuide alloc] init];
//利用导航路径、路径分析对象来初始化路径引导对象,这两个对象在路径分析阶段即可获取
[routeGuide init:route routeAnalysis:routeAnalysis];

2

导航操作

(1)如果进行真实导航:可调用startGNSSNavi和stopGNSSNavi来开始、停止。

//开始真实导航
[routeGuide startGNSSNavi];
//停止真实导航
[routeGuide stopGNSSNavi];

//构建GNSS定位信息对象(需结合定位功能获取真实值)
MGSGNSSLocInfo *gnssLocInfo=[[MGSGNSSLocInfo alloc] init];
gnssLocInfo.longitude=x;      //经度
gnssLocInfo.latitude=y;       //纬度
gnssLocInfo.speed=s;          //速度
gnssLocInfo.hDop=p;           //精度
gnssLocInfo.angle=a;          //角度
//需根据实际定位不断向路径引导对象中推送定位信息
routeGuide.gnssLocInfo=gnssLocInfo;

代码说明:要实现真实导航,必须先实现实时定位功能,然后不断推送给MGSRouteGuide对象。实时定位功能实现,可采用iOS原生定位接口实现,或者利用第三方定位SDK实现(如高德定位SDK、百度定位SDK等)。不管采用哪一种方式进行定位,其接口中一般都会有监听函数,需在其中实例化MGSGNSSLocInfo对象,为其设置相关必要信息,然后设置给MGSRouteGuide对象。

(2)如果进行模拟导航:可调用startSimNavi、pauseOrResumeSimNavi:、stopSimNavi来开始、暂停/继续、停止模拟导航。

//开始模拟导航
[routeGuide startSimNavi];
//暂停模拟导航
[routeGuide pauseOrResumeSimNavi:YES];
//继续模拟导航,暂停了之后才能继续
[routeGuide pauseOrResumeSimNavi:NO];
//停止模拟导航
[routeGuide stopSimNavi];

//加速、减速:改变模拟导航参数
//创建模拟导航的参数信息(模拟速度:公里/小时;生成模拟点的频率:秒)
MGSSimNaviOptions *simNaviOptions=[[MGSSimNaviOptions alloc] initWithSimNaviOptions:100.0f simNaviFrequency:1.0f];
//设置模拟导航的控制参数
[routeGuide setSimNaviOptions:simNaviOptions];

代码说明:模拟导航时需要传入模拟的运动速度,以及生成模拟点的频率,内部会根据这些参数模拟运动效果。

3

导航监听

为路径引导对象设置NavigationDelegate导航相关状态监听器的代理协议,并实现其回调函数,在其中获取导航指引信息,可将其以多种形式展现,如文本控件、图像视图控件等,给用户以提示。

导航,即路径引导,是一个实时、动态的过程,随着用户移动设备位置的变化,导航功能提供的信息也随之变化,所以指引信息也需要动态地获取。SDK目前没有提供默认的导航引导界面,指引信息的展示工作需要开发人员自己编码实现。SDK具有导航状态代理,包括6个回调函数,每个函数提作用都不同,在其中提供了不同的指引信息。需要说明的是,这6个回调函数都是在子线程中执行的,如果获取信息后要操作UI界面,需要在主线程中执行。

//遵守代理协议
@interface OutdoorRouteGuide_ViewController ()<NavigationDelegate>

//设置代理协议
[routeGuide setDelegate:self];

//导航指引信息监听
-(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{
    //这是最重要、使用最多的一个回调函数,可获取诸多导航指引信息,例如当前道路名称、总里程剩余值、导航主动作等等,在下方会详细介绍各种方法如何获取、使用
}

//真实导航状态变更回调监听
-(void)gnssNaviMode:(MGSGNSS_NAVI_MODE)gnssNaviMode statusEx:(int)statusEx{
    switch (gnssNaviMode) {
        case GNSS_NAVI_MODE_START:
            NSLog(@"导航状态:导航开始");
            break;
        case GNSS_NAVI_MODE_ARRIVE_MID:
            NSLog(@"导航状态:到达途径点");
            NSLog(@"到达途经点索引值:%d",statusEx);
            break;
        case GNSS_NAVI_MODE_LEEWAY:
            NSLog(@"导航状态:偏离路径");
            break;
        case GNSS_NAVI_MODE_STOP:
            NSLog(@"导航状态:导航停止");
            break;
        case GNSS_NAVI_MODE_ARRIVE_DEST:
            NSLog(@"导航状态:到达终点");
            break;
        default:
            break;
    }
}

//模拟导航状态变更回调监听
-(void)simNaviMode:(MGSSIM_NAVI_MODE)simNaviMode{
    switch (simNaviMode) {
        case SIM_NAVI_MODE_START:
            NSLog(@"模拟导航状态:模拟导航开始");
            break;
        case SIM_NAVI_MODE_PAUSED:
            NSLog(@"模拟导航状态:模拟导暂停中");
            break;
        case SIM_NAVI_MODE_PROCESSING:
            NSLog(@"模拟导航状态:模拟导航进行中");
            break;
        case SIM_NAVI_MODE_STOP:
            NSLog(@"模拟导航状态:模拟导航停止");
            break;
        case SIM_NAVI_MODE_FINISH:
            NSLog(@"模拟导航状态:模拟导航完成");
            break;
        default:
            break;
    }
}

//处理定位信息监听
-(void)gnssLocInfo:(MGSGNSSLocInfo *)gnssLocInfo matchLng:(double)matchLng matchLat:(double)matchLat matchFloor:(int)matchFloor matchAngle:(float)matchAngle{
    //可获取:卫星状态、卫星数、道路匹配后的坐标、楼层、角度等信息
    double lng=[gnssLocInfo longitude];  //原始经度
    double lat=[gnssLocInfo latitude];   //原始纬度
    NSLog(@"当前GNSS原始信息:经度:%f,纬度:%f",lng,lat);
    NSLog(@"道路匹配后的坐标经度值:%f",matchLng);
    NSLog(@"道路匹配后的坐标纬度值:%f",matchLat);
    NSLog(@"道路匹配后的楼层信息—室内导航专用信息:%d",matchFloor);
    NSLog(@"道路匹配后的角度值:%f",matchAngle);
}

//定位状态监听
-(void)gnssLocStatus:(MGSGNSS_LOC_STATUS)locStatus{
    //获取定位状态:设备是否已连接、已定位等
    switch (locStatus) {
        case GNSS_LOC_STATUS_SEARCHING:
            NSLog(@"定位状态:设备搜索中");
            break;
        case GNSS_LOC_STATUS_FIXING:
            NSLog(@"定位状态:设备已连接,但未定位");
            break;
        case GNSS_LOC_STATUS_FIXED:
            NSLog(@"定位状态:设备已连接并已定位");
            break;
        case GNSS_LOC_STATUS_DISCONNECT:
            NSLog(@"定位状态:设备不可用,如移动端无GNSS模块,或者参数配置失败导致设备无法连接");
            break;
        default:
            break;
    }
}

//播报指引信息监听
-(void)playNaviMessage:(NSString *)message gnssNavi:(BOOL)gnssNavi enPriority:(int)enPriority{
    //获取导航指引语音信息,如:前方200米向左行驶等等
    NSLog(@"播报指引信息:%@",message);
    //特别说明:MapGIS Mobile 10.3 for iOS SDK没有提供内置的语音播报功能,用户可借助第三方开发库实现语音播报
}

4

导航指引信息获取

导航相关状态监听器的代理协议NavigationDelegate中有上述6个回调函数,其中最重要、最常用的函数是导航指引信息的函数routeNaviInfo:gnssNavi:,从此函数中可获取到导航路径信息MGSRouteNaviInfo对象,由此对象获取很多导航过程中非常重要的指引信息。下面将详细介绍信息如何获取与其展示方法:

(1)普通导航指引信息

普通导航指引信息:即界面上提示的当前道路名称、总路程剩余距离、当前行驶速度等信息,这些信息从RouteNaviInfo对象中获取;获取信息后进行界面展示,让用户知道当前行驶的进度与状态。

//导航指引信息回调函数
-(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{
    NSString *currentRoadName=[routeNaviInfo curRoadName];       //当前道路名称
    int currentRoadRemainDistance=[routeNaviInfo segRemainDis];  //当前路段剩余距离
    NSString *nextRoadName=[routeNaviInfo nextRoadName];         //下一道路名称
    int currentSpeed=[routeNaviInfo speed];                      //当前行驶速度
    int remainDistance=[routeNaviInfo remainDis];                //路程总剩余距离
    int remainTime=[routeNaviInfo remainTime];                   //路程总剩余时间
}

对于时间、距离信息,可以将上述接口返回的值进行处理,转换为合适单位的值,从而更加适合用户的查看体验。

//处理距离字符串:以合适的单位显示
-(NSString *)dealDistanceText:(int)distance{
    NSString *strDistance;
    if (distance > 1000) {
        if ((distance % 1000) < 100 || (distance % 1000) == 0) {
            strDistance=[NSString stringWithFormat:@"%.0f公里",((float)distance/1000)];
        } else {
            strDistance=[NSString stringWithFormat:@"%.1f公里",((float)distance/1000)];
        }
    } else {
        strDistance=[NSString stringWithFormat:@"%d米",distance];
    }
    return strDistance;
}

//处理时间字符串:以合适的单位显示
-(NSString *)dealTimeText:(int)time{
    NSString *strTime;
    if (time < 60) {
        strTime=[NSString stringWithFormat:@"%d秒",time];
    } else if (60 < time && time < 3600){
        strTime=[NSString stringWithFormat:@"%.0f分钟",(float)(time/60)];
    } else if (time > 3600){
        strTime=[NSString stringWithFormat:@"%.1f小时",(float)(time/3600)];
    }
    return strTime;
}

上述这些文字信息,开发人员可以在界面上创建一些UILabel文本控件,然后在回调函数中动态修改控件的文本值,即可实现提示作用。需注意这些回调函数都是在子线程中执行的,如果要修改UI控件,需返回到主线程再做操作。

基本导航指引信息.jpg

(2)导航动作信息

导航动作信息:即路径引导中的方向信息,告知用户该向什么方向前进,如左转弯、右转弯等。导航动作信息也是从MGSRouteNaviInfo对象中获取,得到short类型的16进制变量,不同的值对应不同的动作,具体见如下核心代码。

//导航指引信息回调函数
-(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{
    //导航主动作
    short naviAction=[routeNaviInfo naviAction];
    //根据导航主动作信息获取对应的图像
    UIImage *directionImage=nil;
    NSString *directionImageName;
    switch (action) {
            //直行
        case 0x8:
            directionImageName=@"direction_line";
            break;
            //左转
        case 0x1:
            directionImageName=@"direction_zz";
            break;
            //右转
        case 0x2:
            directionImageName=@"direction_yz";
            break;

            ······

        default:
            break;
    }
    directionImage=[UIImage imageNamed:directionImageName];
}

代码说明:获取的导航主动作编码与主动作对应关系为:0x8(继续直行)、0x1(左转)0x2(右转)、0x9(靠左行驶)、0xA(靠右行驶)、0x7(左转调头)、0x0B(进入环岛)、0x0C(离开环岛)。室内专用导航主动作编码:0x10(沿楼梯向上)、0x11(沿楼梯向下)、0x12(沿电梯向上)、0x13(沿电梯向下)、0x14(进入建筑物)、0x15(离开建筑物)。

获取导航动作信息后,需要将此信息通过界面展示给用户,一般通过图片形式展现当前导航动作,能够非常直观地告知用户该往什么方向行驶。要实现此效果,通常在布局设计时添加一个UIImageView图像视图控件,在获取导航动作信息之后,根据对应关系为UIImageView动态设置图片。

导航动作.png

(3)运动目标的当前位置信息

运动目标的当前位置信息:导航过程中另外一个很重要的信息是行人或者车辆当前的位置,一般通过在地图上绘制图标MGSGraphicImage来表示。由于导航是一个动态变化的过程,运动物体的位置、前进的方向在导航过程中是不断变化的,因此需要动态变更其位置点与行进角度。

//导航指引信息回调函数
-(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{
    //定位目标位置经推算后的位置信息
    MGSDot currentPosition=[routeNaviInfo position];
    //定位目标位置推算后的角度信息.与Y轴正方向的夹角,范围为[0,360],逆时针为正
    float angle=[routeNaviInfo angle];
    //地图的旋转角度,单位度,逆时针为正
    float positionAngle=360-angle;

    /*
    绘制图标,作为导航目标的位置。并需要动态修改定位图标的位置,动态地展示导航运动过程
    说明:MGSGraphicImage只需创建一次,后续更新位置只需修改其位置点就行
     */
    if (_locationGraphicImage == nil) {
        _locationGraphicImage=[[MGSGraphicImage alloc] init];
        [_locationGraphicImage setImage:[UIImage imageNamed:@"navigation"]];
        [_locationGraphicImage setAnchorPoint:CGPointMake(0.5f, 0.5f)];
        [_mapView.graphicsOverlay addGraphic:_locationGraphicImage];
    }
    [_locationGraphicImage setPoint:currentPosition];
    
    /*
    动态修改地图的位置:将导航过程中当前位置设置为地图中心,并且旋转地图使得导航视角跟随车头,当然也可以根据需要不修改地图的位置、角度
     */
    MGSMapPosition *mapPosition=[[MGSMapPosition alloc] initWithCenter:currentPosition resolution:[_mapView resolution] rotateCenter:currentPosition rotateAngle:positionAngle slopeAngle:[_mapView slopeAngle]];   //地图位置(中心点,分辨率,旋转中心,旋转角,倾斜角)
    [_mapView updatePosition:mapPosition animated:YES];   //更新位置
    [_mapView refresh];
}

运动目标的地图位置显示效果如下图所示,可以根据路径的方向来改变地图的旋转角度,使导航视角跟随车头,与行人或者车辆真实情况中的朝向一致,营造一定程度的沉浸感。

导航运行目标.png

(4)路口放大图

路口放大图:将路口的分支情况以图片的形式放大展示,特别针对于复杂情况的路口,如典型的环岛,通过路口放大图,可以更加清晰地指引用户该往哪个方向行进。路口放大图的显示,需要自定义一个UIImageView控件,通过SDK中的代码获取UIImage位图,然后赋给图像视图控件,将当前路口放大图展示在界面中。

//导航指引信息回调函数
-(void)routeNaviInfo:(MGSRouteNaviInfo *)routeNaviInfo gnssNavi:(BOOL)gnssNavi{
    //路口放大图背景ID
    int crossBackID=[routeNaviInfo crossBackID];
    //路口放大图箭头ID
    int crossArrowID=[routeNaviInfo crossArrowID];
    //根据背景ID、箭头ID生成路口放大图
    UIImage *crossImage=[self getCrossImageWithCrossBackID:crossBackID andCrossArrowID:crossArrowID];
    //回到主线程
    dispatch_async(dispatch_get_main_queue(), ^{
        if (crossImage != nil) {
            [_ivCrossView setImage:crossImage]; //为UIImageView设置图片
        }
    });
}

//根据背景ID、箭头ID生成路口放大图
-(UIImage *)getCrossImageWithCrossBackID:(int)crossBackID andCrossArrowID:(int)crossArrowID{
    //路口放大图是由路线对象和地图视图生成的
    if (_route == nil || _mapView == nil) {
        return nil;
    }
    //如果剩余路段少于两条时不生成放大图
    if (crossBackID < 0 || crossBackID > _route.stepCount-2) {
        return nil;
    }

    //根据ID获取当前路段、下一路段、下下路段
    MGSDriveWalkSegment *driveWalkSegment=(MGSDriveWalkSegment *)[_route getStep:crossBackID];
    MGSDriveWalkSegment *driveWalkSegmentNext=(MGSDriveWalkSegment *)[_route getStep:(crossBackID+1)];
    MGSDriveWalkSegment *driveWalkSegmentNextNext=nil;
    //获取路段的节点数组、节点数目
    MGSDot *curSegDots=[driveWalkSegment shapes];
    long curSegDotsCount=[driveWalkSegment getPointCount];
    MGSDot *nextSegDots=[driveWalkSegmentNext shapes];
    long nextSegDotsCount=[driveWalkSegmentNext getPointCount];

    //路口放大图imageview的高、宽
    int width=_ivCrossView.frame.size.width;
    int height=_ivCrossView.frame.size.height;
    
    UIImage *image;

    //如果为环岛,处理方式不一样
    if ([driveWalkSegmentNext.form rangeOfString:@"环岛"].location == NSNotFound) {  //为非环岛,普通路口,普通处理
        //利用MGSMapView根据传入的参数生成对应路口的放大图
        image=[_mapView getCrossImageWithSeg1Pnts:curSegDots seg1PntCnt:(int)curSegDotsCount seg2Pnts:nextSegDots seg2PntCnt:(int)nextSegDotsCount seg3Pnts:nil seg3PntCnt:0 imageWidth:width imageHeigth:height];
    } else {  //环岛,需特殊处理
        //如果背景ID大于减3之后的路径路段数,则不构建放大图
        if (crossBackID > (_route.stepCount-3)) {
            return nil;
        }
        //获取当前路段的下下条路段,以及路段的点集、点个数
        driveWalkSegmentNextNext=(MGSDriveWalkSegment *)[_route getStep:(crossBackID+2)];
        MGSDot *nextNextSegDots=[driveWalkSegmentNextNext shapes];
        long nextNextSegDotsCount=[driveWalkSegmentNextNext getPointCount];
        
        //利用MGSMapView根据传入的参数生成对应路口的放大图
        image=[_mapView getCrossImageWithSeg1Pnts:curSegDots seg1PntCnt:(int)curSegDotsCount seg2Pnts:nextSegDots seg2PntCnt:(int)nextSegDotsCount seg3Pnts:nextNextSegDots seg3PntCnt:(int)nextNextSegDotsCount imageWidth:width imageHeigth:height];
    }
    return image;
}

代码说明:路口放大图主要分为普通路口、环岛路口两种,对于环岛需要特殊处理。路口放大图的生成,需要使用MGSMapView地图控件的getCrossBitmap方法实现。例如,普通路口放大图效果如下图所示。

路口放大图.png